include StarbucksDSL
order = latte venti, half_caf, non_fat, no_foam, no_whip
print order.prepare
—Building Domain Specific Languages in Ruby
has_and_belongs_to_many :Bar
validates_presence_of :blitz
some_bars = Bar.find_by_tavern_license(license_number)
Lispers are among the best grads of the Sweep-It-Under-Someone-Else’s-Carpet School of Simulated Simplicity.
—Larry Wall
Kernel
class or creating “top level” methods to be used as verbs in a DSL. You end up with name space crowding: you must be very careful that you do not redefine en existing method.instance_eval
so that it has access to the object’s methods.
I’m trying to get the Zen of building DSLs using Ruby. After reading a dozen or so pieces referenced by my favourite search engine, I have a feeling I’m still not quite getting it.
def bjarne
'Barney'
end
dsl = Object.new
def dsl.phred
'Fred'
end
plus = ' plus '
print dsl.instance_eval {
phred + plus + bjarne
}
##### "Fred plus Barney"
dsl
object, a local variable plus
, and a top-level method bjarne
. We can imagine scaling this up to defining a rich DSL in our DSL object and being able to mix verbs from the DSL with instance variables and other methods as we please.bjarne
in Kernel
. Now bjarne
is essentially global. If we already defined bjarne
somewhere else, we just clobbered it. And if we later run a piece of code that defines bjarne
, we’ll clobber our own version. phred
is different. It’s defined inside of an object, and it doesn’t conflict with any other phred
we define elsewhere.phred
and bjarne
examples of Sandboxing and Top-level methods) and end the post here?MyDsl = Object.new
def MyDsl.phred
'Fred'
end
class ClientCode
def bjarne
'Barney'
end
def friends
plus = ' plus '
MyDsl.instance_eval { phred + plus + bjarne }
end
end
ClientCode.new.friends
##### -:15:in `friends': undefined local variable or method `bjarne' for # (NameError) from -:15:in `friends' from -:20
ClientCode
method. And bjarne
is a method in ClientCode: this way we can continue to separate concerns, keeping phred
inside our DSL and bjarne
inside of the class where we are using the DSL. But it doesn’t work.instance_eval
breaks (in tedious detail)some_object.a_method
), and there is no ambiguity.bjarne
), Ruby tries to find the method for itself. It does so by looking to see whether it is an instance method, in which case it behaves like self.bjarne
. If not, it looks to see whether bjarne
is top-level, in which case it calls that method in the Kernel
. See for yourself:def foo
'top level foo'
end
def bar
'top level bar'
end
class Test
def bar
'instance method bar'
end
def test
p foo
p bar
end
end
Test.new.test
##### "top level foo" "instance method bar"
()
). What’s the problem? Well, I actually mis-described what happens. Here it is again, with more precision:self
, and then for top-level methods if it can’t find anything. Of course, self
is the current object. Unless it isn’t: That’s what instance_eval
does: it evaluates a block but it changes self
to point to its receiver instead of the object where the code is executing. Everything else stays the same. One more example to show the mechanism:def foo
'top level foo'
end
def bar
'top level bar'
end
class Test
def bar
'instance method bar'
end
def blitz
'current object blitz'
end
def test
p foo
p bar
o = Object.new
def o.blitz
'redefined self blitz'
end
p o.instance_eval { blitz }
p o.instance_eval { 'bar within o gives: ' + bar }
end
end
Test.new.test
##### "top level foo" "instance method bar" "redefined self blitz" "bar within o gives: top level bar"
instance_eval
, we route around our current object and all of our methods are ignored within the block. Ruby really only has two levels of scope: whatever belongs to self and whatever belongs to Kernel.instance_eval
doesn’t change the scope for things like local variables, it just points self
elsewhere.
Those who do not learn from the History of Lisp are doomed to repeat it.
DomainSpecificLanguage
, and then you can use methods from your DSL, from your current object, and from the top-level (if you so choose). For example:require 'dsl'
class MyDSL < DomainSpecificLanguage
def bjarne
'Barney'
end
end
class TheGreat
def phred
'Fredrick'
end
def test
plus = ' plus '
MyDSL.eval { p phred + plus + bjarne }
end
end
TheGreat.new.test
##### "Fredrick plus Barney"
kernel
, the method with
. with
replaces the eval
method so you can also say:
with MyDSL do
p phred + plus + bjarne
end
eval
method creates a new instance of your DSL class, so you can track state within an evaluation. For example:class Censor < DomainSpecificLanguage
attr_reader :ok_on_tv
def initialize (given_binding)
super(given_binding)
@ok_on_tv = true
end
def say something
something.split.each do |word|
@ok_on_tv = false if ['feces', 'urine', 'love', 'pudendum', 'fellator', 'oedipus', 'mammaries'].include?(word)
end
end
end
class GeorgeCarlin
def test
Censor.eval {
say "People much wiser than I have said, I'd rather have my son watch a film with two people making love than two people trying to kill one other."
say "And I of course agree. I wish I know who said it first, and I agree with that."
ok_on_tv
}
end
end
p GeorgeCarlin.new.test
##### "false"
eval
cannot take parameters. For this reason, rumour has it that a method called instance_exec
will be added to Ruby in 1.9. (There are some implementations available that work in Ruby 1.8 if you would like to experiment.)with Let do
let :x => 0, :y => 1 do
assert_equal(1, x + y)
let :x => 2 do
assert_equal(3, x + y)
end
assert_equal(0, x)
end
end
with
syntax. In the Let
DSL, there’s a new method called let
. let
creates a new DSL within Let
. You can see that re-declaring x
does not clobber the value in the outer scope. That is because when let
wrote a new DSL, it added x
and y
as methods.x
and y
are methods returning zero and one. Execute some code in that new DSL. That code will create another DSL where x
is a method returning two.”let
defines methods and not local variables, bad things happen when you try to override real local variables. It’s best to use Let
for some things and local variables for others, but not mix the two.S = [ x | x<-[0..], x^2>3 ]
is a list comprehension in Haskell.[x, y, x * y]
given x
is in the range 1..12
and y
is in the range 1..12
. Let’s write that:
require 'comprehension'
class MultiplicationTable
def twelve_by_twelve
with Comprehension::DSL do
list { [x, y, x * y] }.given(:x => 1..12, :y => 1..12)
end
end
end
p MultiplicationTable.new.twelve_by_twelve
##### [[1, 1, 1], [1, 2, 2], [2, 1, 2], [1, 3, 3], [2, 2, 4] ...
list {
[x, y, x * y]
}.given(:x => 1..12,
:y => 1..12)
. I just wrote it this way so you could see that comprehensions work fine inside of methods. You can also use more than one comprehension inside of a single with Comprehension::DSL do...
end
block: see the unit tests for examples.)
class MultiplicationTable
def twelve_by_twelve
with Comprehension::DSL do
list { "#{x} times #{y} is #{x * y}" }.given(:x => 1..12, :y => 1..12)
end
end
end
p MultiplicationTable.new.twelve_by_twelve
##### ["1 times 1 is 1", "1 times 2 is 2", "2 times 1 is 2", "1 times 3 is 3", "2 times 2 is 4", ...
class MultiplicationTable
def twelve_by_twelve_odds
with Comprehension::DSL do
list { "#{x} times #{y} is #{x * y}" }.given(:x => 1..12, :y => 1..12) { (x % 2 == 1) && (y % 2 == 1) }
end
end
end
p MultiplicationTable.new.twelve_by_twelve_odds
##### ... 3 times 5 is 15", "5 times 3 is 15", "7 times 1 is 7", "1 times 9 is 9", ...
class MultiplicationTable
def odds_times_evens
with Comprehension::DSL do
list { "#{x} times #{y} is #{x * y}" }.given(
:x => list { x }.given(:x => 1..12) { x % 2 == 0 } ,
:y => list { x }.given(:x => 1..12) { x % 2 == 1 } )
end
end
end
p MultiplicationTable.new.odds_times_evens
##### ... "2 times 11 is 22", "4 times 9 is 36", "6 times 7 is 42", ...
Let
? Well, Let
builds the scopes needed for evaluating the where clause and the block defining the elements of the list. Yes, we’ve built a DSL on top of a DSL on top of a DSL. Does this seem like weird trickery? I don’t know why. Do you have any idea how many levels of abstraction are responsible for you reading this essay right now?dsl.rb
has been updated to the latest version. I had committed a rather typical manual synchronization error: I copied the latest file to the wrong directory when I first posted this. Thanks, Justin!dsl.rb
. Open Comprehension
. Save the text only as anything you like, as long as it is in the same directory as dsl.rb
: I use comprehension.rb
. Run comprehension.rb
.